package it.aekidna.xmlmapping
{
   import flash.events.EventDispatcher;
   import flash.system.ApplicationDomain;
   import flash.utils.Dictionary;
   import flash.utils.describeType;
   import flash.utils.getDefinitionByName;
   import flash.utils.getQualifiedClassName;
   
   import memorphic.xpath.XPathQuery;
   import memorphic.xpath.model.XPathContext;
   
   import mx.collections.ArrayCollection;
   import mx.core.ClassFactory;
   import mx.core.IFactory;
   import mx.core.IMXMLObject;
   import mx.events.PropertyChangeEvent;
   import mx.utils.StringUtil;
   
   [DefaultProperty("mappings")]
   [Event(name="initMapping", type="it.aekidna.xmlmapping.XmlMappingEvent")]
   public class XmlMapping 
      extends EventDispatcher 
      implements IMXMLObject
   {
      /**
      * keeps a cache of DescribedType objects for performance reasons
      */
      private static var _describedTypesCache : Dictionary;
      private static var _mappingsCache : Dictionary;
      
      
      private static const ERROR_UNDEFINED_TARGET_PROPERTY_CLASS : String = "targetClass must be defined for MapProperty ( targetClass: {0}, targetProperty: {1} )"; 
      private static const ERROR_UNMAPPABLE_MANDATORY_PROPERTY : String = "Property mapping failed ( targetPropertyClass: {0}, targetProperty: {1}, xpath: \"{2}\" )"; 
      private static const ERROR_NULL_TARGET_CLASS : String = "xmlMapping.targetClass cannot be null. Has targetClass getter function been defined for \"{0}\"?"; 
      private static const ERROR_NULL_SOURCE_XML : String = "xml parameter cannot be null: {0}.translate( xml )"; 
      private static const ERROR_TARGET_CLASS_HAS_NO_PUBLIC_PROPERTIES : String = "Target class \"{0}\" has no public properties to map to (1}"; 
      private static const ERROR_CANNOT_MAP_CUSTOM_CLASS_PROPERTY : String = "Cannot map property \"{0}\". mapProperty.mapping \"{1}\" is not a subclass of XmlMapping";
      private static const ERROR_NON_NUMERIC_VALUE_MAPPED_TO_NUMERIC_PROPERTY : String = "Attempt to map a non numeric value to a numeric property ( targetClass: {0}, targetProperty: {1}, value: \"{2}\" )";
      private static const ERROR_PARSER_CLASS_IS_NOT_IXMLMAPPINGPARSER : String = "parserClass is not an IXmlMappingParser ( mappingClass: {0}, targetProperty: {1} )";
      private static const ERROR_INCOMPATIBLE_OVERRIDING_TARGET_CLASS : String = "Incompatible overriding targetClass \"{0}\". Expected sublcass of \"{1}\"";
      private static const ERROR_MULTIPLE_NODES_FOUND_FOR_NON_COLLECTION_TARGET_PROPERTY : String = "Multiple nodes found on xml for non-collection property \"{0}\".";

      public var dispatchEvents : Boolean = false;
      
      private var _targetClass : Class = Object;

      public var mappings : Array;
      
      public var variables : Array;
      
      public var version : String = "*";
      
      private var _xpathContext : XPathContext;
      
      private var _context : XmlMappingContext;
      
      //private var _outObject : Object;
      
      private var _target : Object;
      
      private var _xml : XML;
      
      public function set target( inTarget : Object ) : void
      {
      	if( inTarget == null )
      	{
      		onError(ERROR_NULL_SOURCE_XML, getQualifiedClassName( this ) );
      	}
      	_target = inTarget;
      	_targetClass = getObjectClass( _target );
      }
      
      public function get target() : Object
      {
      	return _target;
      }
      
      public function get context() : XmlMappingContext
      {
         return _context;
      }
      
      public function get xml() : XML
      {
         return _xml;
      }
      
       
      public function translateList( xmlList : XMLList, mappingContext : XmlMappingContext = null) : Array
      {
         if( mappingContext == null )
         {
            mappingContext = new XmlMappingContext();
         }
         _context = mappingContext;
         if( xmlList.length() > 0 ) 
         {
            var outArray : Array = [];
            for each( var child : XML in xmlList )
            {
               outArray.push( translate( child, _context ) );
            }
            return outArray;
         }
         return null;
      }
      
      public function translate( xml : XML, mappingContext : XmlMappingContext = null ) : Object
      {
      	 var outObject : Object;
         if( mappingContext == null )
         {
            mappingContext = new XmlMappingContext();
         }
         _xml = xml;
         _context = mappingContext;
         _context.XMLMAPPING_PRIVATE::init( this );
         dispatchEvent( new XmlMappingEvent( XmlMappingEvent.INIT_MAPPING, mappingContext ) );
         var actualTargetClass : Class = _targetClass;
         if( xml == null )
         {
            onError(ERROR_NULL_SOURCE_XML, getQualifiedClassName( this ) );
         }
         if( !_targetClass )
         {
            onError(ERROR_NULL_TARGET_CLASS, getQualifiedClassName( this ) ); 
         }

         _createXPathContext( xml );
         var classInfo : Dictionary = _registerClass( actualTargetClass, outObject );
         if( _target == null )
         {
         	outObject = new actualTargetClass();
         }
         else
         {
         	outObject = _target;
         }
         var type : DescribedType;
         var mapPropertyEvent : XmlMapPropertyEvent;
         var targetIsVariable : Boolean = false;
         var propertyValue : *;
         for each( var property : MapProperty in _resolveMappingProperties( outObject ) )
         {
            targetIsVariable = property.targetProperty.match( /^\$\w+/ ) != null;
            //map only if property's targetClass is a subclass of this mapping's target class
            if( version != property.version && property.version != "*" )
            {
               continue;
            }
            if( property.targetClass is ClassFactory && 
               !classIs( actualTargetClass, ClassFactory( property.targetClass ).generator ) )
            {
                 continue;
            }
            //--------------------------------------------------------------------------------
            
            //dispatches a XmlMapPropertyEvent event for conditional mapping
            mapPropertyEvent = new XmlMapPropertyEvent( 
               XmlMapPropertyEvent.BEFORE_PROPERTY_MAP,
               _context );
            property.dispatchEvent( mapPropertyEvent );
            if( !mapPropertyEvent.shouldMap )
            {
               continue;
            }
            //--------------------------------------------------------------------------------
            
            if( targetIsVariable )
            {
               type = new DescribedType( ClassFactory( property.targetPropertyClass).generator );
            }
            else
            {
               type = DescribedType( classInfo[ property.targetProperty ] );
            }
            var resolvedNode : Object = resolveNode( xml, property.source );
            if( resolvedNode == null && !property.mandatory )
            {
               continue;
            }
            if( resolvedNode is XMLList && 
               type.classObj != Array &&
               type.classObj != ArrayCollection &&
               XMLList( resolvedNode ).length() > 1 )
            {
               onError( ERROR_MULTIPLE_NODES_FOUND_FOR_NON_COLLECTION_TARGET_PROPERTY, 
                  property.targetProperty );
            }

            if( property.mandatory && resolvedNode == null )
            {
              onError( ERROR_UNMAPPABLE_MANDATORY_PROPERTY, 
                 getQualifiedClassName( type.classObj ),
                 property.targetProperty,
                 property.source
                 );
            }
            
            // Custom parsing ---------------------------------------------------------------------------
            if( property.willTrigger( XmlMapPropertyParseEvent.PARSE ) &&
            property.parserClass != null )
            {
            	//TODO: refine this
            	onError( "You cannot define both a parser class and a parse event handler" );
            }
            if( property.willTrigger( XmlMapPropertyParseEvent.PARSE ) )
            {
            	property.dispatchEvent( new XmlMapPropertyParseEvent( XmlMapPropertyParseEvent.PARSE, 
            		_context,
            		resolvedNode ) );
            		continue;
            }
            if( property.parserClass != null )
            {
               var parser : IXmlMappingParser;
               var classFactory : ClassFactory = ClassFactory( property.parserClass );
               if( !classIs( classFactory.generator, IXmlMappingParser ) )
               {
                  onError( ERROR_PARSER_CLASS_IS_NOT_IXMLMAPPINGPARSER,
                     getQualifiedClassName( this ),
                     property.targetProperty );
               } 
               if( classFactory.generator == prototype.constructor )
               {
                  parser = this as IXmlMappingParser;
               }
               else
               {
                  parser = classFactory.newInstance() as IXmlMappingParser;
               }
               propertyValue = parser.parse( property.targetProperty, outObject, resolvedNode, property.parserContext);
            } // ------------------------------------------------------------------------------------------
            else
            { 
                // Array types 
               if( type.classObj == Array || type.classObj == ArrayCollection )
               {
                  var a : Object = describeType( type.classObj );
                  var collection : ArrayCollection = new ArrayCollection();
                  var collectionObject : Object;
                  for each(var xmlObj : XML in resolveNode( xml, property.source ) )
                  {
                     if( property.mapping == null )
                     {
                        // Base type Array
                        var cName : String = getQualifiedClassName( ClassFactory( property.targetPropertyClass ).generator );
                        var innerType : DescribedType = _createDescribedType( cName );
                        collectionObject = _processProperty( property, innerType, actualTargetClass, xmlObj, outObject );
                     }
                     else 
                     {
                        // Custom type Array
                        collectionObject = _processProperty( property, type, actualTargetClass, xmlObj, outObject );
                     }
                     if( collectionObject is Ignorable )
                     {
                        continue
                     }
                     collection.addItem( collectionObject );
                  }
                  if ( type.classObj == Array )
                  {
                     propertyValue = collection.toArray();
                  }
                  else
                  {
                  	 
                     propertyValue = collection;
                  }
               }
              else
              {
              //Process non array property
                propertyValue = _processProperty( property, type, actualTargetClass, resolvedNode[0] as XML, outObject );
              }
            }
            _context.XMLMAPPING_PRIVATE::mappedProperties[ property ] = propertyValue;
            if( targetIsVariable )
            {
               context.XMLMAPPING_PRIVATE::setVariable( property.targetProperty.substr(1), propertyValue );
            }
            else
            {
               if( outObject[ property.targetProperty ] is ArrayCollection )
               	for each( var item : * in propertyValue  )
               	{
               		ArrayCollection( outObject[ property.targetProperty ] ).addItem( item );
               	}
               else
	               outObject[ property.targetProperty ] = propertyValue;
            }
         }
         _context.XMLMAPPING_PRIVATE::cleanUp();
         return outObject;
      }

      private function _resolveMappingProperties( inOutObject : Object ) : Array
      {
         var outMappings : Array = [];
         for each( var mapping : Object in mappings )
         {
            if( mapping is MapProperty )
            {
               outMappings.push( mapping );
            }
            else if( mapping is MapPropertyGroup )
            {
               outMappings = outMappings.concat( MapPropertyGroup( mapping ).getProperties() );
            } 
         }
         for each( var property : MapProperty in outMappings )
         {
            property.XMLMAPPING_PRIVATE::_parentMapping = this;
            property.addEventListener( 
            	PropertyChangeEvent.PROPERTY_CHANGE, 
            	function( inEvent : PropertyChangeEvent ) : void 
            	{
            		_onPropertyValueChange( inEvent, inOutObject );
            	} );
         }
         return outMappings;
      }
      
      private function _onPropertyValueChange( event : PropertyChangeEvent, inOutObject : Object ) : void
      {
         inOutObject[ MapProperty( event.target ).targetProperty ] = event.newValue;
      }
      

      private function _registerClass( inTargetClass : Class, inOutObject : Object ) : Dictionary
      {
         if( _describedTypesCache == null )
         {
            _describedTypesCache = new Dictionary();
         }
         //DescribedType objects are created analyzing targetClass when targetClass is an user-defined class.
         //When targetClass is Object, they are created using MapProperty objects' information instead.
         var className : String = 
            inTargetClass != Object ?
               getQualifiedClassName( inTargetClass ) :
               getQualifiedClassName( this ); 
         if( _describedTypesCache.hasOwnProperty( className ) )
         {
            return _describedTypesCache[ className ] as Dictionary;
         }
         var properties : Dictionary = new Dictionary();
         if( inTargetClass != Object )
         {
            var propsCount : int;
            var typeXml : XML = describeType( inTargetClass );
            //get description of public variables
            for each( var variable : XML in typeXml.factory.variable )
            {
               properties[ variable.@name.toString() ] = _createDescribedType( variable.@type );
               propsCount ++;
            }
            //get description of public read/write accessors
            for each( var accessor : XML in typeXml.factory.accessor.( 
               @access == "readwrite" || @access == "read" ) )
            {
               properties[ accessor.@name.toString() ] = _createDescribedType( accessor.@type );
               propsCount ++;
            }
            if( propsCount == 0 )
            {   
               onError( ERROR_TARGET_CLASS_HAS_NO_PUBLIC_PROPERTIES,
                  getQualifiedClassName( inTargetClass ),
                  getQualifiedClassName( this ) );
            } 
         }
         else 
         {
            //create descriptors using the information
            //provided in mappings which entries must define "destionationClass"
            for each( var property : MapProperty in _resolveMappingProperties( inOutObject ) )
            {  
               var targetPropertyClass : Class;
               try {
                  targetPropertyClass = ClassFactory( property.targetPropertyClass ).generator;
               }
               catch( e : Error )
               { 
                  onError( ERROR_UNDEFINED_TARGET_PROPERTY_CLASS, 
                      property.targetPropertyClass,
                     property.targetProperty);
               }
              properties[ property.targetProperty ] = _createDescribedType( getQualifiedClassName( targetPropertyClass ) ); 
            }
         }
         _describedTypesCache[ className ] = properties;
         return properties;
      }
      
      
      private function _createDescribedType( typeName : String ) : DescribedType
      {
         var type : DescribedType;
         switch( typeName )
         {
            case "int":
            case "uint":
               type = new DescribedType(
                  null, true, typeName );
               break;
            default:
               type = new DescribedType(
                 Class( getDefinitionByName( typeName ) ) );
            break;
         }
         return type;
      } 
      
      private function _processProperty( property : MapProperty, type : DescribedType, targetClass : Class, xml : XML, inOutObject : Object ) : Object
      {
           if( type.isPrimitive )
           {  
              var val : Number = Number( xml.toString() );
              if( ( type.primitiveType == "int" || type.primitiveType ) == "uint" && isNaN( val ) )
              {
                 if( property.mandatory )
                 {
                    onError( ERROR_NON_NUMERIC_VALUE_MAPPED_TO_NUMERIC_PROPERTY,
                       getQualifiedClassName( targetClass ),
                       property.targetProperty,
                       property.source );
                 }
                 else
                 {
                    return Ignorable.IGNORABLE;
                 }
             }
             switch( type.primitiveType )
             {
                case "int":
                   return int( val.toString() );
                   break;
                case "uint":
                   return uint( val.toString() );
                   break;
             }
           }
           else if( type.classObj == String )
           {
              return xml.toString();
           }
           else if( type.classObj == Boolean )
           {
              return Boolean( xml.toString() );
           } 
           else if( type.classObj == Number )
           {
              var resolvedNumber : Number = Number( xml.toString() );
              if( isNaN( resolvedNumber ) )
              {
                 if( property.mandatory )
                 {
                    onError( ERROR_NON_NUMERIC_VALUE_MAPPED_TO_NUMERIC_PROPERTY,
                       getQualifiedClassName( targetClass ),
                       property.targetProperty,
                       property.source );
                 }
                 else
                 {
                    return Ignorable.IGNORABLE;
                 }
              }
              return resolvedNumber;
           }
           else if( type.classObj == Date )
           {
              // check Date.parse() documentation for supported formats
              // for non-supported source formats set an IXmlMappingParser for MapProperty.parser
              inOutObject[ property.targetProperty ] = 
                 Date.parse( xml.toString() );
           }
           else
           {
              return _processCustomClassProperty( 
                 property, 
                 xml ); 
           }
         return Ignorable.IGNORABLE;
      }
     
      private function _processCustomClassProperty( property : MapProperty, xml : XML ) : Object
      {
          var mapping : XmlMapping; 
          var mappingClass : Class;
          if( property.mapping is Class )
          {
             if( classIs( Class ( property.mapping ),  XmlMapping ) )
             {
                mappingClass = property.mapping as Class; 
                mapping = new mappingClass() as XmlMapping;
             }
          }
          else if( property.mapping is XmlMapping )
          {
             mapping = property.mapping as XmlMapping;
          }
          else if( property.mapping is String )
          {
             //TODO: catch exception
             mappingClass = Class( getDefinitionByName( property.mapping.toString() ) );
             mapping = new mappingClass() as XmlMapping;
          }
          if( mapping == null )
          {
             onError( ERROR_CANNOT_MAP_CUSTOM_CLASS_PROPERTY,
               property.targetProperty,
               property.mapping == null ? 
                  "null" : 
                  getQualifiedClassName( property.mapping ) );
          }
          return mapping.translate( xml );
      }
      
      public function resolveNode( inXml : XML, inPath : String ) : Object
      {
         var query : XPathQuery = new XPathQuery( inPath, _xpathContext );
         var out : Object = query.exec( inXml );
         if( out.length() == 0 )
         {
            return null;
         }
         return out;
      }
      
      private function _createXPathContext( inXml : XML ) : void
      {
         _xpathContext = new XPathContext();
         for each( var ns : Namespace in inXml.namespaceDeclarations() )
         {
            _xpathContext.namespaces[ ns.prefix ] = ns.uri;
         }
         initXPathContext( _xpathContext );
      }
      
      
      public function initialized(document:Object, id:String):void
      {
      }
      
      public function set targetClass( value : IFactory ) : void
      {
         if( value != null )
         {
           if( !classIs( ClassFactory( value ).generator, _targetClass ) )
           {
              onError( ERROR_INCOMPATIBLE_OVERRIDING_TARGET_CLASS,
                 getQualifiedClassName( ClassFactory( value ).generator ),
                 getQualifiedClassName( _targetClass ) );
           }
            _targetClass = ClassFactory( value ).generator;
         }
      }
     
      /**
      * override this to customize the XPath context
      */
      protected function initXPathContext( inXPathContext : XPathContext ) : void
      {
      }
     
     
      private function onError( message : String,  ... params ) : void
      {
         throw new Error( StringUtil.substitute( message, params ) );
      }

      public static function strictIs( object : Object, rightClass : Class ) : Boolean
      {
         var d : Object =  describeType( object );
         var list : XMLList =  describeType( object ).extendsClass.(@type == getQualifiedClassName( rightClass ) );
         var c1 : Boolean = object is rightClass;
         var c2 : Boolean = list.length() == 0;
         return object is rightClass && list.length() == 0;
      }
     
      public static function classIs( leftClass : Class, rightClass : Class ) : Boolean
      {
         if( leftClass == rightClass )
         {
            return true;
         }
         var list : XMLList =  describeType( leftClass ).factory.extendsClass.(@type == getQualifiedClassName( rightClass ) );
         if( list.length() == 0 )
         {
            list =  describeType( leftClass ).factory.implementsInterface.(@type == getQualifiedClassName( rightClass ) );
         }
         return list.length() > 0;
      }
      
		public static function getObjectClass( inObject : Object ) : Class 
		{
			var outClass : Class;
			var className : String = getQualifiedClassName( inObject );
			var domain : ApplicationDomain = ApplicationDomain.currentDomain;
			
			while( !domain.hasDefinition( className ) )
			{
					if ( domain.parentDomain != null ) {
						domain = domain.parentDomain;
					} else {
						break;
					}
			}
	    	return domain.getDefinition( className ) as Class;
		}
   }
   
}
 
class DescribedType {
   public var classObj : Class;
   public var isPrimitive : Boolean = false;
   public var primitiveType : String;
   public var arrayType : Class = String;
   
   public function DescribedType( 
      classObj : Class,  
      isPrimitive : Boolean = false, 
      primitiveType : String = null
      )
   {
      this.classObj = classObj;
      this.isPrimitive = isPrimitive;
      this.primitiveType = primitiveType;
   }
}

class Ignorable {
   public static const IGNORABLE : Ignorable = new Ignorable();
}